call,apply,bind方法的总结

前言

call 、apply 、bind 是 Function 对象自带的三个方法,可以改变函数体内部 this 的指向。三者第一个参数都是this要指向的对象,非严格模式下,如果没有这个参数或参数为undefined或null,则默认指向全局window。

call()

使用一个指定的this值和若干个指定的参数值的前提下调用某个函数或方法。

call的用处简而言之就是可以让 call() 中的对象调用当前对象所拥有的function,比如Object.prototype.toString.call([]),就是 call 中的 Array 对象调用了当前 Object 对象上的方法。

语法 fun.call(thisArg[, arg1[, arg2[, ...]]])

thisArg

在fun函数运行时指定的this值。需要注意的是下面几种情况

  1. 不传,或者传nullundefined, 函数中的this指向window对象
  2. 传递另一个函数的函数名,函数中的this指向这个函数的引用,并不一定是该函数执行时真正的this
  3. 值为原始值(数字,字符串,布尔值)的this会指向该原始值的自动包装对象,如 StringNumberBoolean
  4. 传递一个对象,函数中的this指向这个对象

arg1, arg2, …

指定的参数列表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function a(){
//输出函数a中的this对象
console.log(this);
}
//定义函数b
function b(){}

var obj = {name:'这是一个屌丝'}; //定义对象obj
a.call(); //window
a.call(null); //window
a.call(undefined);//window
a.call(1); //Number
a.call(''); //String
a.call(true); //Boolean
a.call(b);// function b(){}
a.call(obj); //Object

apply()

语法与 call() 方法的语法几乎完全相同,唯一的区别在于,apply的第二个参数必须是一个包含多个参数的数组(或类数组对象)。

语法:fun.apply(thisArg[, argsArray])

注意: 需要注意:Chrome 14 以及 Internet Explorer 9 仍然不接受类数组对象。如果传入类数组对象,它们会抛出异常。

thisArg

同上callthisArg参数。

argsArray

一个数组或者类数组对象,其中的数组元素将作为单独的参数传给 fun 函数。如果该参数的值为nullundefined,则表示不需要传入任何参数。从ECMAScript 5 开始可以使用类数组对象。

call、apply 区别

  • call 接收的是参数列表
  • apply 接收的是参数数组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var a ={
name : "Cherry",
fn : function (a,b) {
console.log( a + b)
}
}
var b = a.fn;
b.call(a,1,2) // 3

var a ={
name : "Cherry",
fn : function (a,b) {
console.log( a + b)
}
}
var b = a.fn;
b.apply(a,[1,2]) // 3

如果传入一个原始值(字符串、布尔或数字类型)来当做this的绑定对象, 这个原始值会被转换成它的对象形式(new String()),这通常被称为“装箱”。

call、apply 使用场景

1. 根据自己的需要灵活修改this指向

1
2
3
4
5
6
7
8
9
10
var foo = {
name: 'joker',
showName: function () {
console.log(this.name);
}
}
var bar = {
name: 'rose'
}
foo.showName.call(bar);

2. 将类数组对象转换为数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function exam(a, b, c, d, e) {

// 先看看函数的自带属性 arguments 什么是样子的
console.log(arguments);

// 使用call/apply将arguments转换为数组, 返回结果为数组,arguments自身不会改变
var arg = [].slice.call(arguments);

console.log(arg);
}

exam(2, 8, 9, 10, 3);

// result:
// { '0': 2, '1': 8, '2': 9, '3': 10, '4': 3 }
// [ 2, 8, 9, 10, 3 ]
//
// 也常常使用该方法将DOM中的nodelist转换为数组
// [].slice.call( document.getElementsByTagName('li') );

3. 实现继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 定义父级的构造函数
var Person = function (name, age) {
this.name = name;
this.age = age;
this.gender = ['man', 'woman'];
}

// 定义子类的构造函数
var Student = function (name, age, high) {

// use call
Person.call(this, name, age);
this.high = high;
}
Student.prototype.message = function () {
console.log('name:' + this.name + ', age:' + this.age + ', high:' + this.high + ', gender:' + this.gender[0] + ';');
}

new Student('xiaom', 12, '150cm').message();

// result
// ----------
// name:xiaom, age:12, high:150cm, gender:man;

在Student的构造函数中,借助call方法,将父级的构造函数执行了一次,相当于将Person中的代码,在Sudent中复制了一份,其中的this指向为从Student中new出来的实例对象。call方法保证了this的指向正确,因此就相当于实现了继承。Student的构造函数等同于下。

1
2
3
4
5
6
7
var Student = function (name, age, high) {
this.name = name;
this.age = age;
this.gender = ['man', 'woman'];
// Person.call(this, name, age); 这一句话,相当于上面三句话,因此实现了继承
this.high = high;
}

4. 在向其他执行上下文的传递中,确保this的指向保持不变

如下面的例子中,我们期待的是getA被obj调用时,this指向obj,但是由于匿名函数的存在导致了this指向的丢失,在这个匿名函数中this指向了全局,因此我们需要想一些办法找回正确的this指向。

1
2
3
4
5
6
7
8
9
10
var obj = {
a: 20,
getA: function () {
setTimeout(function () {
console.log(this.a)
}, 1000)
}
}

obj.getA();

常规的解决办法很简单,就是使用一个变量,将this的引用保存起来。我们常常会用到这方法,但是我们也要借助上面讲到过的知识,来判断this是否在传递中被修改了,如果没有被修改,就没有必要这样使用了。

1
2
3
4
5
6
7
8
9
var obj = {
a: 20,
getA: function () {
var self = this;
setTimeout(function () {
console.log(self.a)
}, 1000)
}
}

另外就是借助闭包与apply方法,封装一个bind方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function bind(fn, obj) {
return function () {
return fn.apply(obj, arguments);
}
}

var obj = {
a: 20,
getA: function () {
setTimeout(bind(function () {
console.log(this.a)
}, this), 1000)
}
}

obj.getA();

当然,也可以使用ES5中已经自带的bind方法。它与我上面封装的bind方法是一样的效果。

1
2
3
4
5
6
7
8
var obj = {
a: 20,
getA: function () {
setTimeout(function () {
console.log(this.a)
}.bind(this), 1000)
}
}

ES6中也常常使用箭头函数的方式来替代这种方案

bind()

bind() 函数会创建一个新函数(称为绑定函数)

  • bind是ES5新增的一个方法

  • 传参和call或apply类似

  • 不会执行对应的函数,call或apply会自动执行对应的函数

  • 返回对函数的引用

语法 fun.bind(thisArg[, arg1[, arg2[, ...]]])

下面例子:当点击网页时,EventClick被触发执行,输出JSLite.io p1 p2, 说明EventClick中的thisbind改变成了obj对象。如果你将EventClick.bind(obj,'p1','p2') 变成 EventClick.call(obj,'p1','p2') 的话,页面会直接输出 JSLite.io p1 p2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var obj = {name:'JSLite.io'};
/**
* 给document添加click事件监听,并绑定EventClick函数
* 通过bind方法设置EventClick的this为obj,并传递参数p1,p2
*/
document.addEventListener('click',EventClick.bind(obj,'p1','p2'),false);
//当点击网页时触发并执行
function EventClick(a,b){
console.log(
this.name, //JSLite.io
a, //p1
b //p2
)
}
// JSLite.io p1 p2

call、apply 与 bind 区别

  • bind是返回绑定this之后的函数,applycall 则是立即执行
  • 三者都可以传参,且applycall是一次性传入参数,而bind可以分为多次传入
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var a ={
name : "Cherry",
fn : function (a,b) {
console.log( a + b)
}
}
var b = a.fn;
b.bind(a,1,2)() // 3
--- 或 ---
var c = b.bind(a,1,2)
c //注意,返回的是一个函数
//ƒ (a,b) {
// console.log( a + b)
//}
c() // 3

多次绑定 bind 只有第一次是有效的

1
2
3
4
5
6
7
8
9
10
11
var one = function(){
console.log(this.x);
}
var two = {
x: 1
}
var three = {
x: 2
}
var fn = one.bind(two).bind(three);
fn(); //1

因为 bind() 的实现,相当于使用函数在内部包了一个 call/apply,第二次 bind() 相当于再包住第一次 bind(),所以后面的 bind 都是无效的

1
2
3
4
5
var fn = function() {
return function() {
one.apply(two);
}.apply(three);
}

利用 call() 和 apply() 方法实现 bind() 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 未兼容 new 版
if( !function(){}.bind ) {
Function.prototype.bind = function(obj) {
var self = this; //this 是调用函数的对象
var args = Array.prototype.slice.call(arguments); //将 arguments 类数组转换成数组

return function() {
return self.apply(obj, args.slice(1));
}
}
}

// 兼容 new 版
Function.prototype.myBind = function(context) {
//先判断调用函数的对象是否为function,不是就跑出错误。
if ( typeof this !== 'function') {
throw new Error("Function.prototype.bind - what is trying to be bound is not callable")
}
const self = this
//获取传入的参数
const args = [...arguments].slice(1)
//先定义一个空函数进行中转
const fNOP = function () {}
const fBound = function() {
//获取函数执行的时候的参数
const bindArgs = [...arguments]
//判断调用的函数是否为构造函数,如果是构造函数this指向实例,将this绑定到实例
//如果函数不是构造函数,将this指向context
return self.apply( this instanceof fNOP? this : context, [...args, ...bindArgs])
}

fNOP.prototype = this.prototype
//修改返回函数的prototype的时候也会修改绑定函数的prototype,实例就可以继承绑定函数的原型中的值
fBound.prototype = new fNOP()
return fBound
}

参考文章

前端基础进阶(七):全方位解读this

回味JS基础:call apply 与 bind

JavaScript基础心法——call apply bind